Изучите продвинутые возможности TypeScript, такие как шаблонные литеральные и условные типы, для написания более выразительного и поддерживаемого кода. Освойте манипуляции с типами для сложных сценариев.
TypeScript Advanced Types: Освоение шаблонных литеральных и условных типов
Сила TypeScript заключается в его мощной системе типов. В то время как базовые типы, такие как string, number и boolean, достаточны для многих сценариев, продвинутые возможности, такие как шаблонные литеральные типы и условные типы, открывают новый уровень выразительности и типобезопасности. Это руководство предоставляет исчерпывающий обзор этих продвинутых типов, исследуя их возможности и демонстрируя практические примеры использования.
Понимание шаблонных литеральных типов
Шаблонные литеральные типы расширяют возможности литералов шаблонов JavaScript, позволяя определять типы на основе интерполяции строк. Это позволяет создавать типы, представляющие определенные строковые шаблоны, делая ваш код более надежным и предсказуемым.
Базовый синтаксис и использование
Шаблонные литеральные типы используют обратные кавычки (`) для обрамления определения типа, аналогично литералам шаблонов JavaScript. Внутри обратных кавычек вы можете интерполировать другие типы, используя синтаксис ${}. Здесь и происходит магия – вы фактически создаете тип, который является строкой, построенной во время компиляции на основе типов внутри интерполяции.
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIEndpoint = `/api/${string}`;
// Пример использования
const getEndpoint: APIEndpoint = "/api/users"; // Допустимо
const postEndpoint: APIEndpoint = "/api/products/123"; // Допустимо
const invalidEndpoint: APIEndpoint = "/admin/settings"; // TypeScript не покажет ошибку здесь, поскольку `string` может быть чем угодно
В этом примере APIEndpoint — это тип, представляющий любую строку, начинающуюся с /api/. Хотя этот базовый пример полезен, истинная сила шаблонных литеральных типов проявляется при их сочетании с более строгими ограничениями типов.
Сочетание с объединениями типов
Шаблонные литеральные типы по-настоящему раскрывают свой потенциал при использовании с объединениями типов. Это позволяет создавать типы, представляющие определенный набор строковых комбинаций.
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIPath = "users" | "products" | "orders";
type APIEndpoint = `/${APIPath}/${HTTPMethod}`;
// Допустимые конечные точки API
const getUsers: APIEndpoint = "/users/GET";
const postProducts: APIEndpoint = "/products/POST";
// Недопустимые конечные точки API (приведут к ошибкам TypeScript)
// const invalidEndpoint: APIEndpoint = "/users/PATCH"; // Ошибка: "/users/PATCH" не может быть присвоен типу "/users/GET" | "/users/POST" | "/users/PUT" | "/users/DELETE" | "/products/GET" | "/products/POST" | ... еще 3 ... | "/orders/DELETE".
Теперь APIEndpoint — это более строгий тип, который допускает только определенные комбинации путей API и методов HTTP. TypeScript будет сообщать о любых попытках использования недопустимых комбинаций, повышая типобезопасность.
Манипуляции со строками с помощью шаблонных литеральных типов
TypeScript предоставляет встроенные типы для манипуляций со строками, которые беспрепятственно работают с шаблонными литеральными типами. Эти типы позволяют преобразовывать строки во время компиляции.
- Uppercase: Преобразует строку в верхний регистр.
- Lowercase: Преобразует строку в нижний регистр.
- Capitalize: Делает первую букву строки заглавной.
- Uncapitalize: Делает первую букву строки строчной.
type Greeting = "hello world";
type UppercaseGreeting = Uppercase; // "HELLO WORLD"
type LowercaseGreeting = Lowercase; // "hello world"
type CapitalizedGreeting = Capitalize; // "Hello world"
type UncapitalizedGreeting = Uncapitalize; // "hello world"
Эти типы манипуляций со строками особенно полезны для автоматического создания типов на основе соглашений об именовании. Например, вы можете извлекать типы действий из имен событий или наоборот.
Практические примеры использования шаблонных литеральных типов
- Определение конечных точек API: Как показано выше, определение конечных точек API с точными ограничениями типов.
- Обработка событий: Создание типов для имен событий с определенными префиксами и суффиксами.
- Генерация CSS-классов: Генерация имен CSS-классов на основе имен компонентов и их состояний.
- Построение запросов к базе данных: Обеспечение типобезопасности при построении запросов к базе данных.
Международный пример: Форматирование валюты
Представьте, что вы создаете финансовое приложение, поддерживающее несколько валют. Вы можете использовать шаблонные литеральные типы для обеспечения правильного форматирования валюты.
type CurrencyCode = "USD" | "EUR" | "GBP" | "JPY";
type CurrencyFormat = `${number} ${T}`;
const priceUSD: CurrencyFormat<"USD"> = "100 USD"; // Допустимо
const priceEUR: CurrencyFormat<"EUR"> = "50 EUR"; // Допустимо
// const priceInvalid: CurrencyFormat<"USD"> = "100 EUR"; // Ошибка: Тип "string" не может быть присвоен типу "`${number} USD`".
function formatCurrency(amount: number, currency: T): CurrencyFormat {
return `${amount} ${currency}`;
}
const formattedUSD = formatCurrency(250, "USD"); // Тип: "250 USD"
const formattedEUR = formatCurrency(100, "EUR"); // Тип: "100 EUR"
Этот пример гарантирует, что значения валюты всегда форматируются с правильным кодом валюты, предотвращая потенциальные ошибки.
Углубляемся в условные типы
Условные типы вводят логику ветвления в систему типов TypeScript, позволяя определять типы, которые зависят от других типов. Эта функция невероятно мощна для создания высокогибких и повторно используемых определений типов.
Базовый синтаксис и использование
Условные типы используют ключевое слово infer и тернарный оператор (condition ? trueType : falseType) для определения условий типов.
type IsString = T extends string ? true : false;
type StringCheck = IsString; // type StringCheck = true
type NumberCheck = IsString; // type NumberCheck = false
В этом примере IsString — это условный тип, который проверяет, является ли T назначаемым строке string. Если да, тип разрешается в true; в противном случае он разрешается в false.
Ключевое слово infer
Ключевое слово infer позволяет извлекать тип из типа. Это особенно полезно при работе со сложными типами, такими как типы функций или типы массивов.
type ReturnType any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType; // type AddReturnType = number
В этом примере ReturnType извлекает тип возвращаемого значения функции типа T. Часть infer R условного типа выводит тип возвращаемого значения и присваивает его переменной типа R. Если T не является типом функции, тип разрешается в any.
Дистрибутивные условные типы
Условные типы становятся дистрибутивными, когда проверяемый тип является голым параметром типа. Это означает, что условный тип применяется к каждому члену типа объединения отдельно.
type ToArray = T extends any ? T[] : never;
type NumberOrStringArray = ToArray; // type NumberOrStringArray = string[] | number[]
В этом примере ToArray преобразует тип T в тип массива. Поскольку T является голым параметром типа (не обернутым в другой тип), условный тип применяется отдельно к number и string, в результате чего получается объединение number[] и string[].
Практические примеры использования условных типов
- Извлечение типов возвращаемых значений: Как показано выше, извлечение типа возвращаемого значения функции.
- Фильтрация типов из объединения: Создание типа, содержащего только определенные типы из объединения.
- Определение перегруженных типов функций: Создание различных типов функций на основе входных типов.
- Создание условных типов: Определение функций, которые сужают тип переменной.
Международный пример: Обработка различных форматов дат
В разных регионах мира используются разные форматы дат. Вы можете использовать условные типы для обработки этих различий.
type DateFormat = "YYYY-MM-DD" | "MM/DD/YYYY" | "DD.MM.YYYY";
type ParseDate = T extends "YYYY-MM-DD"
? { year: number; month: number; day: number; format: "YYYY-MM-DD" }
: T extends "MM/DD/YYYY"
? { month: number; day: number; year: number; format: "MM/DD/YYYY" }
: T extends "DD.MM.YYYY"
? { day: number; month: number; year: number; format: "DD.MM.YYYY" }
: never;
function parseDate(dateString: string, format: T): ParseDate {
// (Реализация будет обрабатывать различные форматы дат)
if (format === "YYYY-MM-DD") {
const [year, month, day] = dateString.split("-").map(Number);
return { year, month, day, format } as ParseDate;
} else if (format === "MM/DD/YYYY") {
const [month, day, year] = dateString.split("/").map(Number);
return { month, day, year, format } as ParseDate;
} else if (format === "DD.MM.YYYY") {
const [day, month, year] = dateString.split(".").map(Number);
return { day, month, year, format } as ParseDate;
} else {
throw new Error("Неверный формат даты");
}
}
const parsedDateISO = parseDate("2023-10-27", "YYYY-MM-DD"); // Тип: { year: number; month: number; day: number; format: "YYYY-MM-DD"; }
const parsedDateUS = parseDate("10/27/2023", "MM/DD/YYYY"); // Тип: { month: number; day: number; year: number; format: "MM/DD/YYYY"; }
const parsedDateEU = parseDate("27.10.2023", "DD.MM.YYYY"); // Тип: { day: number; month: number; year: number; format: "DD.MM.YYYY"; }
console.log(parsedDateISO.year); // Получение доступа к году, зная, что он там будет
Этот пример использует условные типы для определения различных функций парсинга дат на основе указанного формата даты. Тип ParseDate гарантирует, что возвращаемый объект имеет правильные свойства в зависимости от формата.
Сочетание шаблонных литеральных и условных типов
Настоящая мощь раскрывается при сочетании шаблонных литеральных и условных типов. Это позволяет выполнять невероятно мощные манипуляции с типами.
type EventName = `on${Capitalize}`;
type ExtractEventPayload = T extends EventName
? { type: T; payload: any } // Упрощено для демонстрации
: never;
type ClickEvent = EventName<"click">; // "onClick"
type MouseOverEvent = EventName<"mouseOver">; // "onMouseOver"
// Пример функции, принимающей тип
function processEvent(event: T): ExtractEventPayload {
// В реальной реализации мы бы фактически отправляли событие.
console.log(`Processing event ${event}`);
// В реальной реализации payload зависел бы от типа события.
return { type: event, payload: {} } as ExtractEventPayload;
}
// Обратите внимание, что типы возвращаемых значений очень специфичны:
const clickEvent = processEvent("onClick"); // { type: "onClick"; payload: any; }
const mouseOverEvent = processEvent("onMouseOver"); // { type: "onMouseOver"; payload: any; }
// Если вы используете другие строки, вы получаете `never`:
// const someOtherEvent = processEvent("someOtherEvent"); // Тип: `never`
Лучшие практики и рекомендации
- Сохраняйте простоту: Хотя эти продвинутые типы мощны, они могут быстро стать сложными. Стремитесь к ясности и поддерживаемости.
- Тщательно тестируйте: Убедитесь, что ваши определения типов работают так, как ожидается, написав исчерпывающие модульные тесты.
- Документируйте свой код: Четко документируйте назначение и поведение ваших продвинутых типов, чтобы улучшить читаемость кода.
- Учитывайте производительность: Чрезмерное использование продвинутых типов может повлиять на время компиляции. Профилируйте свой код и оптимизируйте при необходимости.
Заключение
Шаблонные литеральные типы и условные типы — это мощные инструменты в арсенале TypeScript. Овладев этими продвинутыми типами, вы сможете писать более выразительный, поддерживаемый и типобезопасный код. Эти возможности позволяют улавливать сложные взаимосвязи между типами, применять более строгие ограничения и создавать высокоповторно используемые определения типов. Используйте эти методы, чтобы повысить свои навыки TypeScript и создавать надежные и масштабируемые приложения для глобальной аудитории.